package com.spzx.product.service.impl;

import com.spzx.common.core.utils.StringUtils;
import com.spzx.product.service.TestService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Service;

import java.util.Arrays;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

@Slf4j
@Service
public class TestServiceImpl implements TestService {

   @Autowired
   private StringRedisTemplate redisTemplate;
    
   @Override
   public synchronized void testLock() {
      //实现最简单的分布式锁：使用redis
      //尝试从缓存中获取业务数据，如果命中缓存则返回即可，缓存未命中，解决缓存击穿问题，先获取分布式锁
         //1.获取分布式锁
      //版本1：采用Redis提供setnx命令 存在问题：A线程加锁成功后，逻辑执行中服务宕机，锁无法释放造成死锁
       String lockName = "lock";
       //String lockValue = "lock";
       //Boolean flag = redisTemplate.opsForValue().setIfAbsent(lockName, lockValue);
         //版本2：在版本1基础上，通过expire命令为锁的Key设置过期时间，防止死锁 问题：加锁跟设置锁过期时间不具备原子性：也就是加锁成功但是没来得及设置过期时间就宕机了
      //防止死锁，为锁的Key设置过期时间
      //Boolean flag = redisTemplate.expire(lockName, 5, TimeUnit.SECONDS);//设置5秒的过期时间
         //版本3：使用set命令配合 NX EX 参数 替换版本2的命令 确保 写入值跟设置过期时间原子性 【set lock lock NX EX 10】问题：
      //Boolean flag = redisTemplate.opsForValue().setIfAbsent(lockName, lockValue, 5, TimeUnit.SECONDS);
         //版本4：在版本3基础上，存入Redis锁的值，为每个线程生成UUID作为线程标识。
      String lockValue = UUID.randomUUID().toString();
      Boolean flag = redisTemplate.opsForValue().setIfAbsent(lockName, lockValue, 5, TimeUnit.SECONDS);
         //版本5：使用lua脚本删除锁，确保删除锁跟判断锁值一致性
      // 问题：1.编码麻烦  2.底层采用String无法锁可重入  3.加锁后没有锁自动续期机制  版本6：采用生产框架Redisson提供分布式锁
      //2.获取所成功后才执行业务逻辑
      if (flag) {
         try {
            // 查询Redis中的num值
            String value = this.redisTemplate.opsForValue().get("num");
            // 没有该值return
            if (StringUtils.isBlank(value)) {
               return;
            }
            // 有值就转成int
            int num = Integer.parseInt(value);
            // 把Redis中的num值+1
            this.redisTemplate.opsForValue().set("num", String.valueOf(++num));
         } finally {
            //3.释放锁
            //版本四：先判断Redis中锁的值跟当前线程虚拟机栈获取当前线程标识值是否一致，一致则删除锁 问题：判断跟删除锁不是原子操作
//            if(redisTemplate.opsForValue().get(lockName).equals(lockValue)){
//            redisTemplate.delete(lockName);
//            }
            //创建Redis脚本对象，封装lua脚本
            String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1]\n" +
                    "then\n" +
                    "    return redis.call(\"del\",KEYS[1])\n" +
                    "else\n" +
                    "    return 0\n" +
                    "end\n";
            RedisScript<Boolean> booleanRedisScript = new DefaultRedisScript<>(script, Boolean.class);
            redisTemplate.execute(booleanRedisScript, Arrays.asList(lockName), lockValue);
         }
      }else {
         log.error("获取锁失败：{},再次尝试自旋",Thread.currentThread().getName());
         this.testLock();
      }
   }
}